﻿using System;
using System.IO;
using System.Security;
using System.Security.Cryptography;
using System.Text;

namespace Common.Security
{
    /// <summary>
    /// This class uses a symmetric key algorithm (Rijndael/AES) to encrypt and
    /// decrypt data. As long as encryption and decryption routines use the same
    /// parameters to generate the keys, the keys are guaranteed to be the same.
    /// The class uses static functions with duplicate code to make it easier to
    /// demonstrate encryption and decryption logic. In a real-life application,
    /// this may not be the most efficient way of handling encryption, so - as
    /// soon as you feel comfortable with it - you may want to redesign this class.
    /// </summary>
    /// <example><code><![CDATA[
    /// string   plainText           = "Hello, World!";    // original plaintext
    /// string   passPhrase          = "Pas5pr@se";        // can be any string
    /// string   _saltValue          = "s@1tValue";        // can be any string
    /// string   _hashAlgorithm      = "SHA1";             // can be "MD5"
    /// int      _passwordIterations = 2;                  // can be any number
    /// string   _initVector         = "@1B2c3D4e5F6g7H8"; // must be 16 bytes
    /// int      _keySize            = 256;                // can be 192 or 128
    /// ]]></code></example>
    public class CryptoHelper
    {
        private const int       _keySize = 256;
        private readonly byte[] _saltValue     =  Encoding.ASCII.GetBytes("s@1tValue");
        private const string    _hashAlgorithm = "SHA1";
        private const int       _passwordIterations = 2;
        private readonly byte[] _initVector;

        /// <summary>
        /// CTOR
        /// </summary>
        public CryptoHelper()
        {
            var r = new Random(8392);
            _initVector = new byte[16];
            r.NextBytes(_initVector);
        }

        /// <summary>
        /// Encrypts specified plaintext using Rijndael symmetric key algorithm
        /// and returns a base64-encoded result.
        /// </summary>
        /// <param name="stringToEncrypt">string to be encrypted</param>
        /// <param name="passPhrase">Passphrase from which a pseudo-random masterPassword will be derived. The
        /// derived masterPassword will be used to generate the encryption key.
        /// Passphrase can be any string. In this example we assume that this
        /// passphrase is an ASCII string.</param>
        /// <returns>encrypted string</returns>
        public string Encrypt(SecureString stringToEncrypt,
                                     string passPhrase)
        {
            return this.Encrypt(stringToEncrypt.Decode(), passPhrase);
        }

        /// <summary>
        /// Encrypts specified plaintext using Rijndael symmetric key algorithm
        /// and returns a base64-encoded result.
        /// </summary>
        /// <param name="plainText">
        /// Plaintext value to be encrypted.
        /// </param>
        /// <param name="passPhrase">
        /// Passphrase from which a pseudo-random masterPassword will be derived. The
        /// derived masterPassword will be used to generate the encryption key.
        /// Passphrase can be any string. In this example we assume that this
        /// passphrase is an ASCII string.
        /// </param>
        /// <returns>
        /// Encrypted value formatted as a base64-encoded string.
        /// </returns>
        public string Encrypt(string plainText,
                                     string passPhrase)
        {
            // Convert strings into byte arrays.
            // Let us assume that strings only contain ASCII codes.
            // If strings include Unicode characters, use Unicode, UTF7, or UTF8
            // encoding.

            // Convert our plaintext into a byte array.
            // Let us assume that plaintext contains UTF8-encoded characters.
            byte[] plainTextBytes  = Encoding.UTF8.GetBytes(plainText);

            byte[] keyBytes;

            // First, we must create a masterPassword, from which the key will be derived.
            // This masterPassword will be generated from the specified passphrase and
            // salt value. The masterPassword will be created using the specified hash
            // algorithm. MasterPassword creation can be done in several iterations.
            using (var password = new PasswordDeriveBytes(
                                                            passPhrase,
                                                            _saltValue,
                                                            _hashAlgorithm,
                                                            _passwordIterations))
            {
                // Use the masterPassword to generate pseudo-random bytes for the encryption
                // key. Specify the size of the key in bytes (instead of bits).

                keyBytes = password.GetBytes(_keySize / 8);
            }

            ICryptoTransform encryptor;

            // Create uninitialized Rijndael encryption object.
            using (var symmetricKey = new RijndaelManaged())
            {
                // It is reasonable to set encryption mode to Cipher Block Chaining
                // (CBC). Use default options for other symmetric key parameters.
                symmetricKey.Mode = CipherMode.CBC;

                // Generate encryptor from the existing key bytes and initialization
                // vector. Key size will be defined based on the number of the key
                // bytes.

                encryptor = symmetricKey.CreateEncryptor(
                                                                                 keyBytes,
                                                                                 _initVector);
            }

            byte[] cipherTextBytes;
            MemoryStream memoryStream = null;
            CryptoStream cryptoStream = null;

            // Define memory stream which will be used to hold encrypted data.
            // Define cryptographic stream (always use Write mode for encryption).
            try
            {
                memoryStream = new MemoryStream();
                cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);

                // Start encrypting.
                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);

                // Finish encrypting.
                cryptoStream.FlushFinalBlock();

                // Convert our encrypted data from a memory stream into a byte array.
                cipherTextBytes = memoryStream.ToArray();
            }
            finally
            {
                if (cryptoStream != null)
                    cryptoStream.Dispose();
                else
                    if (memoryStream != null)
                        memoryStream.Dispose();
            }

            // Convert encrypted data into a base64-encoded string.
            string cipherText = Convert.ToBase64String(cipherTextBytes);

            // Return encrypted string.
            return cipherText;
        }

        /// <summary>
        /// Decrypts specified ciphertext using Rijndael symmetric key algorithm.
        /// For details <see cref="Decrypt"/>.
        /// </summary>
        /// <param name="cipherText">encrypted string to decrypt</param>
        /// <param name="passPhrase">Base64-formatted ciphertext value.</param>
        /// <returns>decrypted secure string</returns>
        public SecureString DecryptToSecureString(string cipherText,

                                     string passPhrase)
        {
            var secString = new SecureString();
            secString.Append(Decrypt(cipherText, passPhrase));
            return secString;
        }

        /// <summary>
        /// Decrypts specified ciphertext using Rijndael symmetric key algorithm.
        /// </summary>
        /// <param name="cipherText">
        /// Base64-formatted ciphertext value.
        /// </param>
        /// <param name="passPhrase">
        /// Passphrase from which a pseudo-random masterPassword will be derived. The
        /// derived masterPassword will be used to generate the encryption key.
        /// Passphrase can be any string. In this example we assume that this
        /// passphrase is an ASCII string.
        /// </param>
        /// <returns>
        /// Decrypted string value.
        /// </returns>
        /// <remarks>
        /// Most of the logic in this function is similar to the Encrypt
        /// logic. In order for decryption to work, all parameters of this function
        /// - except cipherText value - must match the corresponding parameters of
        /// the Encrypt function which was called to generate the
        /// ciphertext.
        /// </remarks>
        public string Decrypt(string cipherText,
                                     string passPhrase)
        {
            // Convert strings defining encryption key characteristics into byte
            // arrays. Let us assume that strings only contain ASCII codes.
            // If strings include Unicode characters, use Unicode, UTF7, or UTF8
            // encoding.

            // Convert our ciphertext into a byte array.
            byte[] cipherTextBytes = Convert.FromBase64String(cipherText);
            byte[] keyBytes;

            // First, we must create a masterPassword, from which the key will be
            // derived. This masterPassword will be generated from the specified
            // passphrase and salt value. The masterPassword will be created using
            // the specified hash algorithm. MasterPassword creation can be done in
            // several iterations.
            using (var password = new PasswordDeriveBytes(
                                                            passPhrase,
                                                            _saltValue,
                                                            _hashAlgorithm,
                                                            _passwordIterations))
            {
                // Use the masterPassword to generate pseudo-random bytes for the encryption
                // key. Specify the size of the key in bytes (instead of bits).
                keyBytes = password.GetBytes(_keySize / 8);
            }

            ICryptoTransform decryptor;

            // Create uninitialized Rijndael encryption object.
            using (var    symmetricKey = new RijndaelManaged())
            {
                // It is reasonable to set encryption mode to Cipher Block Chaining
                // (CBC). Use default options for other symmetric key parameters.
                symmetricKey.Mode = CipherMode.CBC;

                // Generate decryptor from the existing key bytes and initialization
                // vector. Key size will be defined based on the number of the key
                // bytes.
                decryptor = symmetricKey.CreateDecryptor(
                                                                                 keyBytes,
                                                                                 _initVector);
            }

            int decryptedByteCount;
            byte[] plainTextBytes;
            MemoryStream memoryStream = null;
            CryptoStream cryptoStream = null;

            // Define memory stream which will be used to hold encrypted data.
            // Define cryptographic stream (always use Read mode for encryption).
            try
            {
                memoryStream = new MemoryStream(cipherTextBytes);
                cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);

                // Since at this point we don't know what the size of decrypted data
                // will be, allocate the buffer long enough to hold ciphertext;
                // plaintext is never longer than ciphertext.
                plainTextBytes = new byte[cipherTextBytes.Length];

                // Start decrypting.
                decryptedByteCount = cryptoStream.Read(plainTextBytes,
                                                       0,
                                                       plainTextBytes.Length);
            }
            finally
            {
                if (cryptoStream != null)
                    cryptoStream.Dispose();
                else
                    if (memoryStream != null)
                        memoryStream.Dispose();
            }

            // Convert decrypted data into a string.
            // Let us assume that the original plaintext string was UTF8-encoded.
            string plainText = Encoding.UTF8.GetString(plainTextBytes,
                                                       0,
                                                       decryptedByteCount);

            // Return decrypted string.
            return plainText;
        }
    }
}